package cn.sciento.starter.social.core.security;

import java.io.Serializable;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import cn.sciento.starter.social.core.common.connect.SocialUserData;
import cn.sciento.starter.social.core.exception.UserUnbindException;
import cn.sciento.starter.social.core.provider.Provider;
import cn.sciento.starter.social.core.provider.SocialUserProviderRepository;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.security.SocialAuthenticationToken;
import org.springframework.util.Assert;

public abstract class SocialAuthenticationProvider implements AuthenticationProvider {
  private SocialUserProviderRepository userProviderRepository;
  
  private SocialUserDetailsService userDetailsService;
  
  public SocialAuthenticationProvider(SocialUserProviderRepository userProviderRepository, SocialUserDetailsService userDetailsService) {
    this.userProviderRepository = userProviderRepository;
    this.userDetailsService = userDetailsService;
  }
  
  public boolean supports(Class<?> authentication) {
    return SocialAuthenticationToken.class.isAssignableFrom(authentication);
  }
  
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
    Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
    SocialAuthenticationToken authToken = (SocialAuthenticationToken)authentication;
    Connection<?> connection = authToken.getConnection();
    String providerId = Provider.realProviderId(authToken.getProviderId());
    String providerUserId = connection.getKey().getProviderUserId();
    String providerUnionId = connection.createData().getProviderUnionId();
    String username = toUsernameByProviderUserId(providerId, providerUserId, authToken);
    if (StringUtils.isBlank(username)) {
      username = toUsernameAndAutoBindByUnionId(providerId, providerUnionId, connection);
      if (StringUtils.isBlank(username))
        throw new UserUnbindException("hoth.social.providerNotBindUser"); 
    } 
    UserDetails userDetails = retrieveUser(username, authToken);
    if (userDetails == null)
      throw new UsernameNotFoundException("hoth.social.userNotFound"); 
    return createSuccessAuthentication(connection, userDetails, authToken);
  }
  
  protected abstract UserDetails retrieveUser(String paramString, SocialAuthenticationToken paramSocialAuthenticationToken) throws AuthenticationException;
  
  protected String toUsernameByProviderUserId(String providerId, String providerUserId, SocialAuthenticationToken authentication) {
    if (StringUtils.isBlank(providerUserId))
      return null; 
    String username = this.userProviderRepository.findUsernameByProviderId(providerId, providerUserId);
    return (String)StringUtils.defaultIfBlank(username, null);
  }
  
  protected String toUsernameAndAutoBindByUnionId(String providerId, String providerUnionId, Connection<?> connection) {
    if (StringUtils.isBlank(providerUnionId))
      return null; 
    List<String> usernames = this.userProviderRepository.findUsernameByUnionId(providerId, providerUnionId);
    if (CollectionUtils.isEmpty(usernames))
      return null; 
    if (usernames.stream().distinct().count() < usernames.size())
      throw new IllegalStateException("one user bind multi different union_id."); 
    String username = usernames.get(0);
    ConnectionData data = connection.createData();
    if (StringUtils.isBlank(data.getDisplayName()))
      throw new IllegalArgumentException("social user's open_name should not be null."); 
    SocialUserData socialUser = new SocialUserData(data);
    this.userProviderRepository.createUserBind(username, providerId, connection.getKey().getProviderUserId(), socialUser);
    return username;
  }
  
  protected Authentication createSuccessAuthentication(Connection<?> connection, UserDetails userDetails, SocialAuthenticationToken authToken) {
    return (Authentication)new SocialAuthenticationToken(connection, (Serializable)userDetails, authToken.getProviderAccountData(), userDetails.getAuthorities());
  }
  
  public SocialUserProviderRepository getUserProviderRepository() {
    return this.userProviderRepository;
  }
  
  public SocialUserDetailsService getUserDetailsService() {
    return this.userDetailsService;
  }
}
